package com.hp.ecs.xqa.util;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.text.CharacterIterator;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.text.StringCharacterIterator;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

class JsonWriter {
	private static final Log log = LogFactory.getLog(JsonWriter.class);

	public static final boolean ENUM_AS_BEAN_DEFAULT = false;

	private static char[] hex = "0123456789ABCDEF".toCharArray();

	private final StringBuilder buf = new StringBuilder();

	private final Stack stack = new Stack();

	private boolean ignoreHierarchy = true;

	private Object root;

	private boolean buildExpr = true;

	private String exprStack = "";

	private Collection<Pattern> excludeProperties;

	private DateFormat formatter;

	private boolean enumAsBean = false;

	public String write(Object object) {
		return write(object, null);
	}

	public String write(Object object, Collection<Pattern> excludeProperties) {
		this.buf.setLength(0);
		this.root = object;
		this.exprStack = "";
		this.buildExpr = ((excludeProperties != null) && (!excludeProperties.isEmpty()));
		this.excludeProperties = excludeProperties;
		value(object, null);

		return this.buf.toString();
	}

	private void value(Object object, Method method) {
		if (object == null) {
			add("null");

			return;
		}

		if (this.stack.contains(object)) {
			Class clazz = object.getClass();

			if ((clazz.isPrimitive()) || (clazz.equals(String.class))) {
				process(object, method);
			} else {
				if (log.isDebugEnabled()) {
					log.debug("Cyclic reference detected on " + object);
				}

				add("null");
			}

			return;
		}

		process(object, method);
	}

	private void process(Object object, Method method) {
		this.stack.push(object);

		if ((object instanceof Class)) {
			string(object);
		} else if ((object instanceof Boolean)) {
			bool(((Boolean)object).booleanValue());
		} else if ((object instanceof Number)) {
			add(object);
		} else if ((object instanceof String)) {
			string(object);
		} else if ((object instanceof Character)) {
			string(object);
		} else if ((object instanceof Map)) {
			map((Map)object, method);
		} else if (object.getClass().isArray()) {
			array(object, method);
		} else if ((object instanceof Iterable)) {
			array(((Iterable)object).iterator(), method);
		} else if ((object instanceof Date)) {
			date((Date)object);
		} else if ((object instanceof Calendar)) {
			date(((Calendar)object).getTime());
		} else if ((object instanceof Enum)) {
			enumeration((Enum)object);
		} else {
			bean(object);
		}

		this.stack.pop();
	}

	private void bean(Object object) {
		add("{");
		try {
			Class clazz = object.getClass();

			BeanInfo info = (object == this.root) && (this.ignoreHierarchy) ? Introspector.getBeanInfo(clazz,
				clazz.getSuperclass()) : Introspector.getBeanInfo(clazz);

			PropertyDescriptor[] props = info.getPropertyDescriptors();

			boolean hasData = false;
			for (int i = 0; i < props.length; i++) {
				PropertyDescriptor prop = props[i];
				String name = prop.getName();
				Method accessor = prop.getReadMethod();
				Method baseAccessor = null;
				if (clazz.getName().indexOf("$$EnhancerByCGLIB$$") > -1) {
					try {
						baseAccessor = Class.forName(clazz.getName().substring(0, clazz.getName().indexOf("$$")))
							.getDeclaredMethod(accessor.getName(), accessor.getParameterTypes());
					} catch (Exception ex) {
						log.debug(ex.getMessage());
					}
				} else {
					baseAccessor = accessor;
				}
				if (baseAccessor == null) {
					continue;
				}

				if (shouldExcludeProperty(prop)) {
					continue;
				}
				String expr = null;
				if (this.buildExpr) {
					expr = expandExpr(name);
					if (shouldExcludeProperty(expr)) {
						continue;
					}
					expr = setExprStack(expr);
				}
				if (hasData) {
					add(',');
				}
				hasData = true;

				Object value = accessor.invoke(object, new Object[0]);
				add(name, value, accessor);
				if (this.buildExpr) {
					setExprStack(expr);
				}

			}

			if ((object instanceof Enum)) {
				if (hasData) {
					add(',');
				}
				Object value = ((Enum)object).name();
				add("_name", value, object.getClass().getMethod("name", new Class[0]));
			}
		} catch (Exception e) {
			throw new IllegalArgumentException(e);
		}

		add("}");
	}

	private void enumeration(Enum enumeration) {
		if (this.enumAsBean) {
			bean(enumeration);
		} else {
			string(enumeration.name());
		}
	}

	private boolean shouldExcludeProperty(PropertyDescriptor prop) {
		String name = prop.getName();
		return (name.equals("class")) || (name.equals("declaringClass"));
	}

	private String expandExpr(int i) {
		return this.exprStack + "[" + i + "]";
	}

	private String expandExpr(String property) {
		if (this.exprStack.length() == 0) {
			return property;
		}
		return this.exprStack + "." + property;
	}

	private String setExprStack(String expr) {
		String s = this.exprStack;
		this.exprStack = expr;
		return s;
	}

	private boolean shouldExcludeProperty(String expr) {
		for (Pattern pattern : this.excludeProperties) {
			if (pattern.matcher(expr).matches()) {
				if (log.isDebugEnabled()) {
					log.debug("Ignoring property " + expr);
				}
				return true;
			}
		}
		return false;
	}

	private void add(String name, Object value, Method method) {
		add('"');
		add(name);
		add("\":");
		value(value, method);
	}

	private void map(Map map, Method method) {
		add("{");

		Iterator it = map.entrySet().iterator();

		boolean hasData = false;
		while (it.hasNext()) {
			Map.Entry entry = (Map.Entry)it.next();
			Object key = entry.getKey();
			String expr = null;
			if (this.buildExpr) {
				if (key == null) {
					log.error("Cannot build expression for null key in " + this.exprStack);

					continue;
				}
				expr = expandExpr(key.toString());
				if (shouldExcludeProperty(expr)) {
					continue;
				}
				expr = setExprStack(expr);
			}

			if (hasData) {
				add(',');
			}
			hasData = true;
			value(key, method);
			add(":");
			value(entry.getValue(), method);
			if (this.buildExpr) {
				setExprStack(expr);
			}
		}

		add("}");
	}

	private void date(Date date) {
		if (this.formatter == null) {
			this.formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
		}

		string(formatter.format(date));
	}

	private void array(Iterator it, Method method) {
		add("[");

		boolean hasData = false;
		for (int i = 0; it.hasNext(); i++) {
			String expr = null;
			if (this.buildExpr) {
				expr = expandExpr(i);
				if (shouldExcludeProperty(expr)) {
					continue;
				}
				expr = setExprStack(expr);
			}
			if (hasData) {
				add(',');
			}
			hasData = true;
			value(it.next(), method);
			if (this.buildExpr) {
				setExprStack(expr);
			}
		}

		add("]");
	}

	private void array(Object object, Method method) {
		add("[");

		int length = Array.getLength(object);

		boolean hasData = false;
		for (int i = 0; i < length; i++) {
			String expr = null;
			if (this.buildExpr) {
				expr = expandExpr(i);
				if (shouldExcludeProperty(expr)) {
					continue;
				}
				expr = setExprStack(expr);
			}
			if (hasData) {
				add(',');
			}
			hasData = true;
			value(Array.get(object, i), method);
			if (this.buildExpr) {
				setExprStack(expr);
			}
		}

		add("]");
	}

	private void bool(boolean b) {
		add(b ? "true" : "false");
	}

	private void string(Object obj) {
		add('"');

		CharacterIterator it = new StringCharacterIterator(obj.toString());

		for (char c = it.first(); c != 65535; c = it.next()) {
			if (c == '"') {
				add("\\\"");
			} else if (c == '\\') {
				add("\\\\");
			} else if (c == '/') {
				add("\\/");
			} else if (c == '\b') {
				add("\\b");
			} else if (c == '\f') {
				add("\\f");
			} else if (c == '\n') {
				add("\\n");
			} else if (c == '\r') {
				add("\\r");
			} else if (c == '\t') {
				add("\\t");
			} else if (Character.isISOControl(c)) {
				unicode(c);
			} else {
				add(c);
			}
		}

		add('"');
	}

	private void add(Object obj) {
		this.buf.append(obj);
	}

	private void add(char c) {
		this.buf.append(c);
	}

	private void unicode(char c) {
		add("\\u");

		int n = c;

		for (int i = 0; i < 4; i++) {
			int digit = (n & 0xF000) >> 12;

			add(hex[digit]);
			n <<= 4;
		}
	}

	public void setIgnoreHierarchy(boolean ignoreHierarchy) {
		this.ignoreHierarchy = ignoreHierarchy;
	}

	public void setEnumAsBean(boolean enumAsBean) {
		this.enumAsBean = enumAsBean;
	}
}
